Skip to content

Support class Foo = Bar / module Foo = Bar declarations in RBS#444

Merged
mame merged 1 commit intoruby:masterfrom
rhiroe:support-rbs-class-module-alias
Apr 29, 2026
Merged

Support class Foo = Bar / module Foo = Bar declarations in RBS#444
mame merged 1 commit intoruby:masterfrom
rhiroe:support-rbs-class-module-alias

Conversation

@rhiroe
Copy link
Copy Markdown
Contributor

@rhiroe rhiroe commented Apr 27, 2026

Summary

Implement RBS class alias and module alias declarations (class Foo = Bar, module Foo = Bar).

Builds on #443, which fixed the NoMethodError caused by these declarations being silently skipped without .compact. With that crash gone, the alias was still being dropped — every YAML.load / HashWithIndifferentAccess.foo etc. came back untyped. This PR makes those resolve.

After this PR, module YAML = Psych makes the following resolve identically to Psych:

  • singleton method call: YAML.load(str)
  • nested constant: YAML::SyntaxError
  • mixin: class A; include YAML end

This also unblocks real-world RBS sources such as module YAML = Psych (commonly authored by users), module Sidekiq::ClientMiddleware = Sidekiq::ServerMiddleware (gem_rbs_collection sidekiq), and class HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess (gem_rbs_collection activesupport).

Approach

The change centered on Genv#resolve_cpath:

  • ModuleEntity gains @alias_decls (decl ⇒ target_mod) and @alias_target, with add_alias_decl / remove_alias_decl mirroring the existing module decl lifecycle. The alias decl is also registered as a const on the outer module so const lookup of the alias name succeeds.
  • Genv#resolve_cpath follows alias_target after resolving each cpath segment, with cycle detection to handle module A = B; module B = A.
  • New AST nodes SigClassAliasNode / SigModuleAliasNode register the alias via the standard define / undefine / install lifecycle. They access the alias's own ModuleEntity via outer.inner_modules[cname] directly to avoid being short-circuited by resolve_cpath's alias following.
  • The previously empty when RBS::AST::Declarations::AliasDecl branch in AST.create_rbs_decl is replaced with explicit when ClassAlias / when ModuleAlias branches.

Test plan

  • scenario/rbs/class-module-alias.rb: class alias instance method, module alias singleton method / nested constant / mixin
  • scenario/rbs/class-module-alias-nested.rb: alias declared inside a module (module Outer; module InnerAlias = Inner; end)
  • scenario/rbs/class-module-alias-cycle.rb: cycle (module A = B; module B = A) does not infinite-loop
  • bundle exec rake test — 417 tests, 772 assertions, 0 failures

Known limitations / follow-ups

  • LSP "go to definition" on an aliased name currently jumps to the target module's declaration site, not the alias declaration site. A follow-up PR will add the alias's own source location for LSP.

The empty `when RBS::AST::Declarations::AliasDecl` branch in
`AST.create_rbs_decl` was previously a no-op (the prior fix only avoided
the crash). This commit implements actual semantic resolution for class
and module aliases, so that:

- `module YAML = Psych` makes `YAML.load`, `YAML::SyntaxError`, and
  `include YAML` resolve identically to `Psych.load` etc.
- `class HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess`
  (used in gem_rbs_collection's activesupport RBS) resolves correctly.

Implementation (Option A from the design discussion):

- `ModuleEntity` gains `@alias_decls` (decl => target_mod) and
  `@alias_target`, with `add_alias_decl`/`remove_alias_decl` mirroring
  the existing module decl lifecycle. The alias decl is also registered
  as a const on the outer module so const lookup of the alias name
  succeeds.
- `Genv#resolve_cpath` follows `alias_target` after resolving each cpath
  segment, with cycle detection to handle `module A = B; module B = A`.
- New AST nodes `SigClassAliasNode` / `SigModuleAliasNode` register the
  alias via the standard `define`/`undefine`/`install` lifecycle. They
  access the alias's own `ModuleEntity` via `outer.inner_modules[cname]`
  directly to avoid being short-circuited by `resolve_cpath`'s alias
  following.
- `AST.create_rbs_decl` replaces the empty `when AliasDecl` branch with
  explicit `when ClassAlias` / `when ModuleAlias` branches.

Tests cover singleton method, nested constant, instance method, mixin,
nested-module alias, and the cycle case.
@rhiroe rhiroe force-pushed the support-rbs-class-module-alias branch from 7a9d46e to ba9a78e Compare April 27, 2026 09:04
@mame
Copy link
Copy Markdown
Member

mame commented Apr 28, 2026

@rhiroe Thanks! The PR looks good to me. Is it still marked as Draft because you're planning to do more work on it? If not, I'll go ahead and merge.

@rhiroe rhiroe marked this pull request as ready for review April 28, 2026 13:16
@rhiroe
Copy link
Copy Markdown
Contributor Author

rhiroe commented Apr 28, 2026

@mame I've marked this PR as ready for review.

@mame mame merged commit 283b5d9 into ruby:master Apr 29, 2026
6 checks passed
@mame
Copy link
Copy Markdown
Member

mame commented Apr 29, 2026

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants